#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
最近10天没有创新低的股票筛选器

筛选逻辑：
1. 先确定最近40个交易日的数据（30天基准窗口 + 10天观察窗口）
2. 计算观察窗口之前30天内（不含最近10天）的最低价，作为基准低点
3. 检查最近10天内的每日最低价都没有跌破该基准低点，视为没有“创新低”
4. 输出最近价格、缓冲空间、成交量等参考信息

使用方法：
    python no_new_low_10days_screener.py                 # 筛选所有股票
    python no_new_low_10days_screener.py --limit 100     # 仅扫描前100只股票（调试用）
    python no_new_low_10days_screener.py --stock 000001  # 仅扫描指定股票
    python no_new_low_10days_screener.py --hold-days 8 --lookback-days 25
"""
from __future__ import annotations
import argparse
import logging
import os
import sys
from datetime import datetime

import pandas as pd
import warnings

# 忽略pandas警告
warnings.filterwarnings('ignore', category=pd.errors.SettingWithCopyWarning)

# 添加父目录到路径，便于导入数据库模块
PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.append(PROJECT_ROOT)

from config import DATABASE_CONFIG
from database import DatabaseManager, StockInfoDAO

# 日志配置
LOG_DIR = os.path.join(PROJECT_ROOT, 'logs')
os.makedirs(LOG_DIR, exist_ok=True)

logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s [%(levelname)s] %(message)s',
    handlers=[
        logging.StreamHandler(),
        logging.FileHandler(os.path.join(LOG_DIR, 'no_new_low_10days_screener.log'), encoding='utf-8')
    ]
)
logger = logging.getLogger(__name__)


class NoNewLowScreener:
    """最近10天没有创新低筛选器"""

    def __init__(self):
        self.db = DatabaseManager(DATABASE_CONFIG)
        self.stock_dao = StockInfoDAO(self.db)

    def get_stock_codes(self, limit: int | None = None) -> list[tuple[str, str]]:
        """获取所有活跃股票代码和名称"""
        sql = "SELECT stock_code, stock_name FROM new_stock_info WHERE status = 'active' ORDER BY stock_code"
        if limit:
            sql += f" LIMIT {limit}"

        try:
            results = self.db.execute_query(sql)
            return [(row['stock_code'], row['stock_name']) for row in results]
        except Exception as exc:
            logger.error("获取股票列表失败: %s", exc)
            return []

    def get_stock_daily_data(self, stock_code: str, days: int = 80) -> pd.DataFrame:
        """获取股票最近N天的日线数据"""
        sql = """
        SELECT trade_date, open_price, high_price, low_price, close_price, volume, change_pct
        FROM new_stock_daily
        WHERE stock_code = %s
        ORDER BY trade_date DESC
        LIMIT %s
        """
        try:
            results = self.db.execute_query(sql, (stock_code, days))
        except Exception as exc:
            logger.error("获取股票 %s 数据失败: %s", stock_code, exc)
            return pd.DataFrame()

        if not results:
            return pd.DataFrame()

        df = pd.DataFrame(results)
        df['trade_date'] = pd.to_datetime(df['trade_date'])

        for col in ['open_price', 'high_price', 'low_price', 'close_price', 'volume', 'change_pct']:
            df[col] = pd.to_numeric(df[col], errors='coerce')

        return df.sort_values('trade_date').reset_index(drop=True)

    def check_no_new_low(
        self,
        df: pd.DataFrame,
        hold_days: int = 10,
        lookback_days: int = 30
    ) -> dict | None:
        """检查股票在最近hold_days天是否没有跌破前lookback_days天的最低点"""
        min_required_days = hold_days + lookback_days

        if df.empty or len(df) < min_required_days:
            return None

        df = df.copy()
        df = df.dropna(subset=['low_price'])
        if len(df) < min_required_days:
            return None

        recent_window = df.tail(hold_days)
        lookback_window = df.iloc[-(hold_days + lookback_days):-hold_days]

        if lookback_window.empty or recent_window.empty:
            return None

        baseline_low = lookback_window['low_price'].min()
        if pd.isna(baseline_low) or baseline_low <= 0:
            return None

        # 最近10天内若有任何一天的低点低于基准低点，则判定为“创新低”
        if (recent_window['low_price'] < baseline_low).any():
            return None

        baseline_idx = lookback_window['low_price'].idxmin()
        baseline_row = df.loc[baseline_idx]

        recent_low_idx = recent_window['low_price'].idxmin()
        recent_low_row = df.loc[recent_low_idx]

        latest_row = df.iloc[-1]

        margin_pct = ((recent_low_row['low_price'] - baseline_low) / baseline_low * 100) if baseline_low else None
        recent_range_pct = ((recent_window['high_price'].max() - recent_window['low_price'].min()) /
                            recent_window['low_price'].min() * 100) if recent_window['low_price'].min() > 0 else None

        return {
            'trade_date': latest_row['trade_date'],
            'close_price': round(latest_row['close_price'], 2),
            'latest_low': round(latest_row['low_price'], 2),
            'baseline_low': round(baseline_low, 2),
            'baseline_date': baseline_row['trade_date'],
            'recent_low': round(recent_low_row['low_price'], 2),
            'recent_low_date': recent_low_row['trade_date'],
            'buffer_pct': round(margin_pct, 2) if margin_pct is not None else None,
            'recent_range_pct': round(recent_range_pct, 2) if recent_range_pct is not None else None,
            'avg_change_recent': round(recent_window['change_pct'].mean(), 2) if 'change_pct' in recent_window else None,
            'avg_volume_recent': round(recent_window['volume'].mean(), 2) if 'volume' in recent_window else None,
            'days_since_baseline': (latest_row['trade_date'] - baseline_row['trade_date']).days,
            'days_since_recent_low': (latest_row['trade_date'] - recent_low_row['trade_date']).days,
            'hold_days': hold_days,
            'lookback_days': lookback_days
        }

    def screen_stocks(
        self,
        stock_codes: list[tuple[str, str]] | None = None,
        limit: int | None = None,
        hold_days: int = 10,
        lookback_days: int = 30
    ) -> pd.DataFrame:
        """批量筛选股票"""
        if stock_codes is None:
            stock_codes = self.get_stock_codes(limit=limit)

        if not stock_codes:
            logger.warning("没有股票可供筛选")
            return pd.DataFrame()

        logger.info(
            "开始筛选 %d 只股票：最近%d天未创新低（基于之前%d天低点）",
            len(stock_codes),
            hold_days,
            lookback_days
        )

        results: list[dict] = []

        for idx, (stock_code, stock_name) in enumerate(stock_codes, 1):
            if idx % 50 == 0 or idx == len(stock_codes):
                logger.info("进度：%d/%d (%.1f%%)", idx, len(stock_codes), idx / len(stock_codes) * 100)

            try:
                df = self.get_stock_daily_data(stock_code, days=hold_days + lookback_days + 20)
                result = self.check_no_new_low(df, hold_days=hold_days, lookback_days=lookback_days)
                if result:
                    result['stock_code'] = stock_code
                    result['stock_name'] = stock_name
                    results.append(result)
                    logger.info(
                        "✓ %s %s - 最新价 %.2f, 基准低点 %.2f(%s), 缓冲 %.2f%%",
                        stock_code,
                        stock_name,
                        result['close_price'],
                        result['baseline_low'],
                        result['baseline_date'].strftime('%Y-%m-%d'),
                        result['buffer_pct'] if result['buffer_pct'] is not None else 0
                    )
            except Exception as exc:
                logger.error("处理股票 %s %s 失败: %s", stock_code, stock_name, exc)
                continue

        if not results:
            logger.info("未找到符合条件的股票")
            return pd.DataFrame()

        df_results = pd.DataFrame(results).sort_values('buffer_pct', ascending=False)
        return df_results.reset_index(drop=True)

    def save_results(self, df: pd.DataFrame, output_dir: str = '.') -> tuple[str | None, str | None]:
        """保存筛选结果到CSV和TXT"""
        if df.empty:
            logger.warning("没有结果需要保存")
            return None, None

        os.makedirs(output_dir, exist_ok=True)
        timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
        csv_path = os.path.join(output_dir, f'no_new_low_10days_stocks_{timestamp}.csv')
        txt_path = os.path.join(output_dir, f'stock_codes_no_new_low_10days_{timestamp}.txt')

        try:
            df.to_csv(csv_path, index=False, encoding='utf-8-sig')
            with open(txt_path, 'w', encoding='utf-8') as txt_file:
                for _, row in df.iterrows():
                    txt_file.write(f"{row['stock_code']}\n")
        except Exception as exc:
            logger.error("保存结果失败: %s", exc)
            return None, None

        logger.info("结果已保存到: %s", csv_path)
        logger.info("股票代码列表: %s", txt_path)
        return csv_path, txt_path

    def print_summary(self, df: pd.DataFrame):
        if df.empty:
            return

        logger.info("\n%s", "=" * 80)
        logger.info("筛选结果摘要（共 %d 只股票）", len(df))
        logger.info("%s\n", "=" * 80)

        logger.info(
            "平均缓冲: %.2f%%, 最大缓冲: %.2f%%, 最小缓冲: %.2f%%",
            df['buffer_pct'].mean(),
            df['buffer_pct'].max(),
            df['buffer_pct'].min()
        )

        top_n = min(10, len(df))
        logger.info("\n前%d只股票：", top_n)
        for _, row in df.head(top_n).iterrows():
            logger.info(
                "%s %s | 最新价: %.2f | 基准低点: %.2f (%s) | 最近低点: %.2f (%s) | 缓冲: %.2f%%",
                row['stock_code'],
                row['stock_name'],
                row['close_price'],
                row['baseline_low'],
                row['baseline_date'].strftime('%Y-%m-%d'),
                row['recent_low'],
                row['recent_low_date'].strftime('%Y-%m-%d'),
                row['buffer_pct']
            )


def parse_arguments() -> argparse.Namespace:
    parser = argparse.ArgumentParser(
        description='筛选最近10天未创新低的股票（基于之前30天的最低价）'
    )
    parser.add_argument('--limit', type=int, help='限制扫描的股票数量（调试用）')
    parser.add_argument('--stock', type=str, help='仅扫描指定股票代码')
    parser.add_argument('--hold-days', type=int, default=10, help='观察窗口天数，默认10天')
    parser.add_argument('--lookback-days', type=int, default=30, help='基准窗口天数，默认30天（不含观察窗口）')
    parser.add_argument('--output-dir', type=str, default='.', help='结果输出目录，默认当前目录')
    return parser.parse_args()


def main():
    args = parse_arguments()
    logger.info("=" * 80)
    logger.info(
        "筛选条件：最近%d天内低点未跌破此前%d天的最低价",
        args.hold_days,
        args.lookback_days
    )
    logger.info("=" * 80)

    screener = NoNewLowScreener()

    if args.stock:
        stock_info = screener.stock_dao.get_stock_info_by_code(args.stock)
        if not stock_info:
            logger.error("未找到股票: %s", args.stock)
            return
        stock_codes = [(stock_info['stock_code'], stock_info['stock_name'])]
        logger.info("仅筛选指定股票: %s %s", stock_info['stock_code'], stock_info['stock_name'])
    else:
        stock_codes = None

    start_time = datetime.now()
    df_results = screener.screen_stocks(
        stock_codes=stock_codes,
        limit=args.limit,
        hold_days=args.hold_days,
        lookback_days=args.lookback_days
    )
    end_time = datetime.now()

    screener.print_summary(df_results)

    if not df_results.empty:
        screener.save_results(df_results, output_dir=args.output_dir)
        logger.info("筛选用时：%.2f 秒", (end_time - start_time).total_seconds())

    logger.info("筛选完成。")


if __name__ == '__main__':
    main()

